<?php

namespace GetId3\Write;

use GetId3\Lib\Helper;
use GetId3\GetId3Core;

/////////////////////////////////////////////////////////////////
/// GetId3() by James Heinrich <info@getid3.org>               //
//  available at http://getid3.sourceforge.net                 //
//            or http://www.getid3.org                         //
/////////////////////////////////////////////////////////////////
// See readme.txt for more details                             //
/////////////////////////////////////////////////////////////////
//                                                             //
// write.apetag.php                                            //
// module for writing APE tags                                 //
// dependencies: module.tag.apetag.php                         //
//                                                            ///
/////////////////////////////////////////////////////////////////

/**
 * module for writing APE tags
 *
 * @author James Heinrich <info@getid3.org>
 *
 * @link http://getid3.sourceforge.net
 * @link http://www.getid3.org
 *
 * @uses GetId3\Module\Tag\Apetag
 */
class Apetag
{
    public $filename;
    public $tag_data;
    /**
     * @var bool
     */
    public $always_preserve_replaygain = true;    // ReplayGain / MP3gain tags will be copied from old tag even if not passed in data
    /**
     * @var array
     */
    public $warnings = array(); // any non-critical errors will be stored here
    /**
     * @var array
     */
    public $errors = array(); // any critical errors will be stored here

    /**
     * @return bool
     */
    public function __construct()
    {
        return true;
    }

    /**
     * @return bool
     */
    public function WriteAPEtag()
    {
        // NOTE: All data passed to this function must be UTF-8 format

        $getID3 = new GetId3Core();
        $ThisFileInfo = $getID3->analyze($this->filename);

        if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) {
            if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) {
                // Current APE tag between Lyrics3 and ID3v1/EOF
                // This break Lyrics3 functionality
                if (!$this->DeleteAPEtag()) {
                    return false;
                }
                $ThisFileInfo = $getID3->analyze($this->filename);
            }
        }

        if ($this->always_preserve_replaygain) {
            $ReplayGainTagsToPreserve = array('mp3gain_minmax', 'mp3gain_album_minmax', 'mp3gain_undo', 'replaygain_track_peak', 'replaygain_track_gain', 'replaygain_album_peak', 'replaygain_album_gain');
            foreach ($ReplayGainTagsToPreserve as $rg_key) {
                if (isset($ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]) && !isset($this->tag_data[strtoupper($rg_key)][0])) {
                    $this->tag_data[strtoupper($rg_key)][0] = $ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0];
                }
            }
        }

        if ($APEtag = $this->GenerateAPEtag()) {
            if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
                $oldignoreuserabort = ignore_user_abort(true);
                flock($fp, LOCK_EX);

                $PostAPEdataOffset = $ThisFileInfo['avdataend'];
                if (isset($ThisFileInfo['ape']['tag_offset_end'])) {
                    $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']);
                }
                if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) {
                    $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']);
                }
                fseek($fp, $PostAPEdataOffset, SEEK_SET);
                $PostAPEdata = '';
                if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) {
                    $PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset);
                }

                fseek($fp, $PostAPEdataOffset, SEEK_SET);
                if (isset($ThisFileInfo['ape']['tag_offset_start'])) {
                    fseek($fp, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET);
                }
                ftruncate($fp, ftell($fp));
                fwrite($fp, $APEtag, strlen($APEtag));
                if (!empty($PostAPEdata)) {
                    fwrite($fp, $PostAPEdata, strlen($PostAPEdata));
                }
                flock($fp, LOCK_UN);
                fclose($fp);
                ignore_user_abort($oldignoreuserabort);

                return true;
            }
        }

        return false;
    }

    /**
     * @return bool
     */
    public function DeleteAPEtag()
    {
        $getID3 = new GetId3Core();
        $ThisFileInfo = $getID3->analyze($this->filename);
        if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) {
            if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) {
                flock($fp, LOCK_EX);
                $oldignoreuserabort = ignore_user_abort(true);

                fseek($fp, $ThisFileInfo['ape']['tag_offset_end'], SEEK_SET);
                $DataAfterAPE = '';
                if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) {
                    $DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']);
                }

                ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']);
                fseek($fp, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET);

                if (!empty($DataAfterAPE)) {
                    fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE));
                }

                flock($fp, LOCK_UN);
                fclose($fp);
                ignore_user_abort($oldignoreuserabort);

                return true;
            }

            return false;
        }

        return true;
    }

    /**
     * @return bool
     */
    public function GenerateAPEtag()
    {
        // NOTE: All data passed to this function must be UTF-8 format

        $items = array();
        if (!is_array($this->tag_data)) {
            return false;
        }
        foreach ($this->tag_data as $key => $arrayofvalues) {
            if (!is_array($arrayofvalues)) {
                return false;
            }

            $valuestring = '';
            foreach ($arrayofvalues as $value) {
                $valuestring .= str_replace("\x00", '', $value)."\x00";
            }
            $valuestring = rtrim($valuestring, "\x00");

            // Length of the assigned value in bytes
            $tagitem = Helper::LittleEndian2String(strlen($valuestring), 4);

            //$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false);
            $tagitem .= "\x00\x00\x00\x00";

            $tagitem .= $this->CleanAPEtagItemKey($key)."\x00";
            $tagitem .= $valuestring;

            $items[] = $tagitem;
        }

        return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false);
    }

    /**
     * @param  type $items
     * @param  type $isheader
     *
     * @return type
     */
    public function GenerateAPEtagHeaderFooter(&$items, $isheader = false)
    {
        $tagdatalength = 0;
        foreach ($items as $itemdata) {
            $tagdatalength += strlen($itemdata);
        }

        $APEheader = 'APETAGEX';
        $APEheader .= Helper::LittleEndian2String(2000, 4);
        $APEheader .= Helper::LittleEndian2String(32 + $tagdatalength, 4);
        $APEheader .= Helper::LittleEndian2String(count($items), 4);
        $APEheader .= $this->GenerateAPEtagFlags(true, true, $isheader, 0, false);
        $APEheader .= str_repeat("\x00", 8);

        return $APEheader;
    }

    /**
     * @param  type $header
     * @param  type $footer
     * @param  type $isheader
     * @param  type $encodingid
     * @param  type $readonly
     *
     * @return type
     */
    public function GenerateAPEtagFlags($header = true, $footer = true, $isheader = false, $encodingid = 0, $readonly = false)
    {
        $APEtagFlags = array_fill(0, 4, 0);
        if ($header) {
            $APEtagFlags[0] |= 0x80; // Tag contains a header
        }
        if (!$footer) {
            $APEtagFlags[0] |= 0x40; // Tag contains no footer
        }
        if ($isheader) {
            $APEtagFlags[0] |= 0x20; // This is the header, not the footer
        }

        // 0: Item contains text information coded in UTF-8
        // 1: Item contains binary information °)
        // 2: Item is a locator of external stored information °°)
        // 3: reserved
        $APEtagFlags[3] |= ($encodingid << 1);

        if ($readonly) {
            $APEtagFlags[3] |= 0x01; // Tag or Item is Read Only
        }

        return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]);
    }

    /**
     * @param  type $itemkey
     *
     * @return type
     */
    public function CleanAPEtagItemKey($itemkey)
    {
        $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey);

        // http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html
        switch (strtoupper($itemkey)) {
            case 'EAN/UPC':
            case 'ISBN':
            case 'LC':
            case 'ISRC':
                $itemkey = strtoupper($itemkey);
                break;

            default:
                $itemkey = ucwords($itemkey);
                break;
        }

        return $itemkey;
    }
}
